/*
 * RecordsFile.java
 *
 * Created on December 9, 2005, 4:02 PM
 *
 */

package biofilter.sources;

import biofilter.exceptions.InvalidRecordException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.FileReader;
import java.util.logging.Level;
import java.util.logging.Logger;
import biofilter.records.*;

/**
 * To provide a generic RecordsSourceBase that handles Arrays of objects or an input
 * file with one line per record.  This object can be extended and readRecordString
 * can be overridden to handle files with other formats such as multi-line records.
 *
 * @author yan, tom, rox
 */
public class RecordsFile extends RecordsSourceBase {
    private Record[] records        = null;
    private int currentIndex        = 0;
    protected BufferedReader handle   = null;
    protected String filename         = null;
    protected String currentRecordString      = null;
    private boolean fileBased       = false;
    protected boolean eof             = false;
    private Class recordClass       = null;
    private String  recordClassName = null;
    private String headerKeyword 	= null;
    protected Logger logger             = null;
    
    /** Creates a new instance of RecordsFile
     * @param Object either String source - file name containing one-line records
     *        or
     * Record[] source - array-based record set
     * @param className  String, the fully qualified className of the Record class
     */
    public RecordsFile(Object source, String className) throws InvalidRecordException {
        this.logger = Logger.getLogger(this.getClass().getName());
        this.createRecordClass( className);
        this.setSource(source);
    }
    
    /** Creates a new instance of RecordsFile
     * @param Object either String source - file name containing one-line records
     *        or
     * Record[] source - array-based record set
     * @param className  String, the fully qualified className of the Record class
     * @param headerKeyword String, the keywoed search pattern
     */
    public RecordsFile(Object source, String className, String headerKeyword) throws InvalidRecordException {
        this.logger = Logger.getLogger(this.getClass().getName());
        this.setHeaderKeyword(headerKeyword);
        this.createRecordClass( className);
        this.setSource(source);
    }
    
    /**
     * Set the className of the records contained in this file object.  Validate that
     * the classname is valid and can be created via the getInstance() method.
     * @param String the class name of the type of class which is returned by this object
     */
    public void createRecordClass( String recordClassName) throws InvalidRecordException {
        this.recordClassName = recordClassName;
        try {
            this.recordClass = Class.forName( recordClassName);
            this.recordClass.newInstance();
        }
        catch (Exception e) {
            logger.log(Level.SEVERE, "could not create new instance of " + recordClassName, e);
            throw new InvalidRecordException("Fatal "+ e.getClass().getName() + ": " + e.getMessage(), true);
        }
    }
    
    /**
     * Return the className of records contained in this file object
     * @return String - the className used to create records
     */
    public String getRecordClassName() {
        return this.recordClassName;
    }
    
    /**
     * Return the next record in the recordSet
     * @return Record
     */
    public Record getRecord() throws InvalidRecordException {
        Record record = null;
        String line = null;
        if ( !eof())
            if(fileBased) {
                line = readRecordString();
                
                if(line == null) {
                    try {
                        handle.close();
                    } catch (IOException ioe) {
                        throw new InvalidRecordException("Error closing file handle: " + ioe.getMessage());
                    }
                }
                else {
                    try {
                        record = (Record) recordClass.newInstance();
                        record.set( line);
                    } catch (IllegalArgumentException iae) {
                        throw iae;
                    }
                    catch (InstantiationException ine) {
                        throw new InvalidRecordException(ine.getMessage(),true);
                    }
                    catch (IllegalAccessException iae) {
                        throw new InvalidRecordException(iae.getMessage(), true);
                    }
                }
            } else {
                record = records[currentIndex];
                currentIndex++;
                this.eof = ( currentIndex >= this.records.length );
            }
        return record;
    }
    
    /**
     * This sets the source and allows for either a String that is the name (and
     * path) of a file, or an array of Record objects.
     * @param source Either a file name of an array of records. If it is a name
     * of a file (a String), then this file will be used as the source of data from
     * which to construct records, otherwise source will be treated as an array
     * of Records.
     * @throws IllegalArgumentException If the type of Record in the array
     * version does not match what is stored in the recordClass attribute.
     */
    public void setSource(Object source) throws IllegalArgumentException {
        if ( source instanceof String)
            this.setSource( source.toString());
        else
            this.setSource((Record[]) source);
    }
    
    /**
     * Set the records contained in this records file object
     * @param Record[] records - an array containing the objects
     */
    private void setSource(Record[] records) throws IllegalArgumentException {
        this.clearRecords();
        this.fileBased = false;
        
        for ( Object o: records) {
            if ( o == null) {
                continue;
            }
            
            if ( !recordClass.isInstance( o)) {
                throw new IllegalArgumentException( "Invalid Record - " +
                " Expected " + recordClassName + ", found " +
                o.getClass().getName());
            }
        }
        if(records.length <=0){
            this.eof = true;
        }
        this.records = records;
    }
    
    /**
     * Set source to a file
     * @param String filename - the name of the source file
     */
    private void setSource(String filename) throws IllegalArgumentException {
        this.filename = filename;
        this.fileBased = true;
        try {
            this.handle = new BufferedReader( new FileReader( filename ) );
            readRecordString();
        }
        catch ( IOException ioe ) {
            throw new IllegalArgumentException ( "Could not get FileReader for " + filename, ioe);
        }
    }
    
    /**
     * Clear records vector and reset current index
     */
    public void clearRecords() {
        records = null;
        currentIndex = 0;
    }
    
    /**
     * To reset the source to the beginning of the input
     */
    public void resetSource() throws IllegalArgumentException {
        eof = false;
        if ( fileBased) {
            try {
                handle.close();
            } catch (IOException ioe) {
                logger.log(Level.WARNING, "could not close " +filename, ioe);
            }
            handle = null;
            this.setSource( filename);
        } else{
            if(records.length <=0){
                this.eof = true;
            }
            currentIndex = 0;
        }
    }
    
    /**
     * Return the value fo the fileBased class variable
     * @return boolean - true if source if file, false if not
     */
    public boolean isFileBased() {
        return this.fileBased;
    }
    
    /**
     * To read a line from the file, when fileBased. You will override this
     * in a subclass when you are dealing with a multi-line string record.
     * @return String - a string representing one record
     */
    protected String readRecordString() throws InvalidRecordException {
        String oldRecordString = null;
        
        if(currentRecordString!= null)
            oldRecordString = currentRecordString;
        
        String emptyString = "";
        try {
            while( ((currentRecordString = handle.readLine()) != null) &&
            ( (currentRecordString.trim().equals(emptyString))
            || skip( currentRecordString))) {
            }
            
            if ( currentRecordString == null)
                eof = true;
        }
        catch(IOException e) {
            throw new InvalidRecordException( e.getMessage(), true);
        }
        return oldRecordString;
    }
    
    /**
     * Returns true if the end of the record set has been reached, false if not
     * @return boolean  - true if the end of the record set has been reached, false if not
     */
    public boolean eof() {
        return this.eof;
    }
    
    /**
     * Return the next record, without incrementing the counter or actually taking
     * @return Record - the next record which will be returned when getRecord is called again
     */
    @Override
    public Record peek() throws InvalidRecordException {
        if ( eof()) {
            return null;
        }
        else {
            if ( fileBased) {
                Record r = null;
                try {
                    r = (Record) recordClass.newInstance();
                }
                catch (InstantiationException ine) {
                    throw new InvalidRecordException(ine.getMessage(), true);
                }
                catch (IllegalAccessException iae) {
                    throw new InvalidRecordException(iae.getMessage(), true);
                }
                r.set( currentRecordString);
                return r;
            }
            else {
                return records[currentIndex];
            }
        }
    }
    
    /**
     * Indicates that the current record should be skipped.  Probably a header.
     * This is a default, which is not to skip anything.
     * @return boolean - true if the string should be ignored, false if it should not
     */
    protected boolean skip( String currentRecordString) {
        if ( headerKeyword != null){
            return ((currentRecordString.matches("(?i).*" + headerKeyword +".*")) ||
            (currentRecordString.matches("(?i).*well.*")) ||
            (currentRecordString.matches("(?i).*template.*")));
        }
        else
            return ( (currentRecordString.matches("(?i).*well.*")) ||
            (currentRecordString.matches("(?i).*template.*")));
    }
    
    /**
     * Sets a keyword search pattern that will indicate	a row contains a header
     * @param String header keyword
     */
    public void setHeaderKeyword( String headerKeyword) {
        this.headerKeyword = headerKeyword;
    }
    
    /**
     * Get the current file header keyword
     * @return String - header keyword
     */
    public String getHeaderKeyword( ) {
        return this.headerKeyword;
    }
}
